#!/usr/bin/env bash

set -euo pipefail

if [ -n "${DEBUG:-}" ]; then
  set -x
fi

UNAME="$(uname -s)"

PROJ_ROOT="$(git rev-parse --show-toplevel)"
cd "$PROJ_ROOT"

logerr() {
    if [ "${TERM:-dumb}" = dumb ]; then
        echo -e "ERROR: $*" 1>&2
    else
        echo -e "$(tput setaf 1)ERROR: $*$(tput sgr0)" 1>&2
    fi
}

usage() {
cat <<EOF

Run EMQX without building a release (which takes longer time).
Node state is stored in '_build/dev-run/$PROFILE'.
The node is started in interactive mode without a boot file.

USAGE: $0 [COMMAND] [OPTION[]

COMMANDS:
  help:   Print this usage info.
  run:    Default command.
  remsh:  Attach to running node's remote console.
          Target node name is to be specified with -n|--name,
          otherwise defaults to 'emqx@127.0.0.1'.
  ctl:    Equivalent to 'emqx ctl'.
          ctl command arguments should be passed after '--'
          e.g. $0 ctl -- help
  eval:   Evaluate an Erlang expression

OPTIONS:
  -h|--help:         Print this usage info.
  -p|--profile:      emqx | emqx-enterprise, defaults to 'PROFILE' env.
  -c|--compile:      Force recompile, otherwise starts with the already built libs
                     in '_build/\$PROFILE/lib/'.
  -e|--ekka-epmd:    Force to use ekka_epmd.
  -n|--name:         Node name, defaults to \$EMQX_NODE__NAME or the \$EMQX_NODE_NAME env.
  -s|--shortnames:   Use shortnames for erlang node name (ie. use -sname parameter).

ENVIRONMENT VARIABLES:

  PROFILE:           Overridden by '-p|--profile' option, defaults to 'emqx'.
  EMQX_NODE_NAME:    Overridden by '-n|--name' or '-r|--remsh' option.
                     The node name of the EMQX node. Default to emqx@127.0.0.1'.
  EMQX_NODE_COOKIE:  Erlang cookie, defaults to ~/.erlang.cookie

EOF
}

if [ -n "${DEBUG:-}" ]; then
    set -x
fi

export HOCON_ENV_OVERRIDE_PREFIX='EMQX_'
case "${EMQX_DEFAULT_LOG_HANDLER:-console}" in
    console|default)
        export EMQX_LOG__FILE__DEFAULT__ENABLE='false'
        export EMQX_LOG__CONSOLE__ENABLE='true'
        ;;
    file)
        export EMQX_LOG__FILE__DEFAULT__ENABLE='true'
        export EMQX_LOG__CONSOLE__ENABLE='false'
        ;;
    both)
        export EMQX_LOG__CONSOLE__ENABLE='true'
        export EMQX_LOG__FILE__ENABLE='true'
        ;;
    *)
        ;;
esac

SYSTEM="$(./scripts/get-distro.sh)"
if [ -n "${EMQX_NODE_NAME:-}" ]; then
    export EMQX_NODE__NAME="${EMQX_NODE_NAME}"
fi
EMQX_NODE_NAME="${EMQX_NODE__NAME:-emqx@127.0.0.1}"
PROFILE="${PROFILE:-emqx}"
FORCE_COMPILE=0
# Do not start using ekka epmd by default, so your IDE can connect to it
EKKA_EPMD=0
SHORTNAMES=0
COMMAND='run'
case "${1:-novalue}" in
    novalue)
        ;;
    run)
        shift
        ;;
    remsh)
        COMMAND='remsh'
        shift
        ;;
    ctl)
        COMMAND='ctl'
        shift
        ;;
    eval)
        COMMAND='eval'
        shift
        ;;
    help)
        usage
        exit 0
        ;;
    -*)
        ;;
esac

while [ "$#" -gt 0 ]; do
    case $1 in
        -h|--help)
            usage
            exit 0
            ;;
        -n|--name)
            EMQX_NODE_NAME="$2"
            shift 1
            ;;
        -p|--profile)
            PROFILE="${2}"
            shift 1;
            ;;
        -c|--compile)
            FORCE_COMPILE=1
            ;;
        -e|--ekka-epmd)
            EKKA_EPMD=1
            ;;
        -s|--shortnames)
            SHORTNAMES=1
            ;;
        --)
            shift
            PASSTHROUGH_ARGS=("$@")
            break
            ;;
        *)
            logerr "Unknown argument $1"
            usage
            exit 1
            ;;
    esac
    shift 1;
done

case "${PROFILE}" in
    ce|emqx)
        PROFILE='emqx'
        ;;
    ee|emqx-enterprise)
        PROFILE='emqx-enterprise'
        ;;
    ce-ex|emqx-elixir)
        export IS_ELIXIR=yes
        PROFILE='emqx'
        ;;
    ee-ex|emqx-enterprise-elixir)
        export IS_ELIXIR=yes
        PROFILE='emqx-enterprise'
        ;;
    *)
        echo "Unknown profile $PROFILE"
        exit 1
        ;;
esac
export PROFILE

case "${PROFILE}" in
    emqx|emqx-elixir)
        export SCHEMA_MOD='emqx_conf_schema'
        ;;
    emqx-enterprise|emqx-enterprise-elixir)
        export SCHEMA_MOD='emqx_enterprise_schema'
        ;;
esac

BASE_DIR="_build/dev-run/$PROFILE"
export EMQX_ETC_DIR="$BASE_DIR/etc"
export EMQX_DATA_DIR="$BASE_DIR/data"
export EMQX_LOG_DIR="$BASE_DIR/log"
export EMQX_PLUGINS__INSTALL_DIR="${EMQX_PLUGINS__INSTALL_DIR:-$BASE_DIR/plugins}"
CONFIGS_DIR="$EMQX_DATA_DIR/configs"
# Use your cookie so your IDE can connect to it.
COOKIE="${EMQX_NODE__COOKIE:-${EMQX_NODE_COOKIE:-$(cat ~/.erlang.cookie 2>/dev/null || echo 'emqxsecretcookie')}}"
mkdir -p "$EMQX_ETC_DIR" "$EMQX_DATA_DIR/patches" "$EMQX_DATA_DIR/plugins" "$EMQX_DATA_DIR/certs" "$EMQX_LOG_DIR" "$CONFIGS_DIR"
if [ $EKKA_EPMD -eq 1 ]; then
    EPMD_ARGS='-start_epmd false -epmd_module ekka_epmd'
else
    EPMD_ARGS=''
fi

if [ $SHORTNAMES -eq 1 ]; then
    ERL_NAME_ARG='-sname'
else
    ERL_NAME_ARG='-name'
fi


## build compile the profile is it's not compiled yet
prepare_erl_libs() {
    local profile="$1"
    local libs_dir="_build/${profile}/lib"
    local erl_libs="${ERL_LIBS:-}"
    local sep
    if [ "${SYSTEM}" = 'windows' ]; then
        sep=';'
    else
        sep=':'
    fi
    if [ $FORCE_COMPILE -eq 1 ] || [ ! -d "$libs_dir" ]; then
        make "compile-${PROFILE}"
    else
        echo "Running from code in $libs_dir"
    fi
    for app in "${libs_dir}"/*; do
        if [ -n "$erl_libs" ]; then
            erl_libs="${erl_libs}${sep}${app}"
        else
            erl_libs="${app}"
        fi
    done
    if [ "${IS_ELIXIR:-}" = "yes" ]; then
        local elixir_libs_root_dir
        elixir_libs_root_dir=$(realpath "$(elixir -e ':code.lib_dir(:elixir) |> IO.puts()')"/..)
        for app in "${elixir_libs_root_dir}"/*; do
            if [ -n "$erl_libs" ]; then
                erl_libs="${erl_libs}${sep}${app}"
            else
                erl_libs="${app}"
            fi
        done
    fi
    if [ -e "_build/${profile}/checkouts" ]; then
        for app in "_build/${profile}/checkouts"/*; do
            erl_libs="${erl_libs}${sep}${app}"
        done
    fi
    export ERL_LIBS="$erl_libs"
}

## poorman's mustache templating
mustache() {
    local name="$1"
    local value="$2"
    local file="$3"
    if [[ "$UNAME" == "Darwin" ]]; then
        sed -i '' "s|{{[[:space:]]*${name}[[:space:]]*}}|${value}|g" "$file"
    else
        sed -i "s|{{\s*${name}\s*}}|${value}|g" "$file"
    fi
}

## render the merged boot conf file.
## the merge action is done before the profile is compiled
render_hocon_conf() {
    input="apps/emqx_conf/etc/emqx.conf.all"
    output="$EMQX_ETC_DIR/emqx.conf"
    cp "$input" "$output"
    mustache emqx_default_erlang_cookie "$COOKIE" "$output"
    mustache platform_data_dir "${EMQX_DATA_DIR}" "$output"
    mustache platform_log_dir "${EMQX_LOG_DIR}" "$output"
    mustache platform_etc_dir "${EMQX_ETC_DIR}" "$output"
}

## Make comma separated quoted strings
make_erlang_args() {
    local in=("$@")
    local args=''
    for arg in "${in[@]}"; do
        if [ -z "$args" ]; then
            args="\"$arg\""
        else
            args="$args, \"$arg\""
        fi
    done
    echo "$args"
}

call_hocon() {
    local args erl_code
    args="$(make_erlang_args "$@")"
    erl_code="
        {ok, _} = application:ensure_all_started(hocon), \
        try
          mnesia_hook:module_info()
        catch _:_->
          io:format(standard_error, \"Force setting DB backend to 'mnesia', and 'role' to 'core'~n\", []),
          os:putenv(\"EMQX_NODE__DB_BACKEND\", \"mnesia\"),
          os:putenv(\"EMQX_NODE__DB_ROLE\", \"core\")
        end,
        {Time, ok} = timer:tc(fun() -> ok = hocon_cli:main([$args]) end),
        io:format(user, \"Took ~pms to generate config~n\", [Time div 1000]),
        init:stop().
    "
    erl -noshell -eval "$erl_code"
}

# Function to generate app.config and vm.args
# sets two environment variables CONF_FILE and ARGS_FILE
generate_app_conf() {
    ## timestamp for each generation
    local NOW_TIME
    NOW_TIME="$(date +'%Y.%m.%d.%H.%M.%S')"

    ## This command populates two files: app.<time>.config and vm.<time>.args
    ## It takes input sources and overlays values in below order:
    ##   - $DATA_DIR/cluster.hocon (if exists)
    ##   - etc/emqx.conf
    ##   - environment variables starts with EMQX_ e.g. EMQX_NODE__ROLE
    ##
    ## NOTE: it's a known issue that cluster.hocon may change right after the node boots up
    ##       because it has to sync cluster.hocon from other nodes.
    call_hocon -v -t "$NOW_TIME" -s "$SCHEMA_MOD" \
        -c "$EMQX_DATA_DIR"/configs/cluster.hocon \
        -c "$EMQX_ETC_DIR"/emqx.conf \
        -d "$EMQX_DATA_DIR"/configs generate

    ## filenames are per-hocon convention
    CONF_FILE="$CONFIGS_DIR/app.$NOW_TIME.config"
    ARGS_FILE="$CONFIGS_DIR/vm.$NOW_TIME.args"
}

# apps/emqx/etc/vm.args.cloud
append_args_file() {
    ## ensure a new line at the end
    echo '' >> "$ARGS_FILE"
    cat <<EOF >> "$ARGS_FILE"
$ERL_NAME_ARG $EMQX_NODE_NAME
-mnesia dir '"$EMQX_DATA_DIR/mnesia/$EMQX_NODE_NAME"'
-stdlib restricted_shell emqx_restricted_shell
+spp true
+A 4
+IOt 4
+SDio 8
-shutdown_time 30000
-pa '$EMQX_DATA_DIR/patches'
-mnesia dump_log_write_threshold 5000
-mnesia dump_log_time_threshold 60000
-os_mon start_disksup false
EOF
}

# copy cert files and acl.conf to etc
copy_other_conf_files() {
    cp -r apps/emqx/etc/certs "$EMQX_ETC_DIR"/
    cp apps/emqx_auth/etc/acl.conf "$EMQX_ETC_DIR"/
}

is_current_profile_app() {
    local app="$1"
    case "$app" in
        *emqx_telemetry*)
            if [ "$PROFILE" = 'emqx-enterprise' ]; then
                return 1
            else
                return 0
            fi
            ;;
        *)
            if [ "$PROFILE" = 'emqx' ]; then
                if [ -f "$app"/BSL.txt ]; then
                    return 1
                else
                    return 0
                fi
            else
                return 0
            fi
            ;;
    esac
}

## apps to load
apps_to_load() {
    local apps csl
    apps="$(./scripts/find-apps.sh | xargs)"
    csl=""
    for app in $apps; do
        if ! is_current_profile_app "$app"; then
            continue
        fi
        name="$(basename "$app")"
        if [ -z "$csl" ]; then
            csl="$name"
        else
            csl="$csl,$name"
        fi
    done
    echo "$csl"
}

boot() {
    ## Make erl command aware where to load all the beams
    ## this should be done before every erl command
    prepare_erl_libs "$PROFILE"
    ## make sure copy acl.conf and certs to etc before render_hocon_conf
    ## hocon will check rules inside acl.conf.
    copy_other_conf_files
    render_hocon_conf
    generate_app_conf
    append_args_file
    APPS="$(apps_to_load)"

    if [ "${IS_ELIXIR:-}" = "yes" ]; then
        BOOT_SEQUENCE='
          apps = System.get_env("APPS") |> String.split(",") |> Enum.map(&String.to_atom/1)
          Enum.each(apps, &Application.load/1)
          IO.inspect(apps, label: :loaded, limit: :infinity)
          {:ok, _} = Application.ensure_all_started(:emqx_machine)
        '
        if [ -n "${EPMD_ARGS:-}" ]; then
            EPMD_ARGS_ELIXIR="$EPMD_ARGS"
        else
            EPMD_ARGS_ELIXIR="-no_op true"
        fi
        local OTP_VSN USER_MOD_ARG
        OTP_VSN=$(./scripts/get-otp-vsn.sh)
        case "$OTP_VSN" in
          25*)
            USER_MOD_ARG='-user Elixir.IEx.CLI'
            ;;
          *)
            USER_MOD_ARG='-user elixir'
            ;;
        esac

        # shellcheck disable=SC2086
        env APPS="$APPS" iex \
          -$ERL_NAME_ARG "$EMQX_NODE_NAME" \
          --erl "$EPMD_ARGS_ELIXIR" \
          --erl "$USER_MOD_ARG" \
          --erl '-proto_dist ekka' \
          --vm-args "$ARGS_FILE" \
          --erl-config "$CONF_FILE" \
          -e "$BOOT_SEQUENCE"
    else
        BOOT_SEQUENCE="
            Apps=[${APPS}],
            ok=lists:foreach(fun application:load/1, Apps),
            io:format(user, \"~nLoaded: ~p~n\", [Apps]),
            {ok, _} = application:ensure_all_started(emqx_machine).
        "

        # shellcheck disable=SC2086
        erl $ERL_NAME_ARG "$EMQX_NODE_NAME" \
            $EPMD_ARGS \
            -proto_dist ekka \
            -args_file "$ARGS_FILE" \
            -config "$CONF_FILE" \
            -s emqx_restricted_shell set_prompt_func \
            -eval "$BOOT_SEQUENCE"
    fi
}

# Generate a random id
gen_tmp_node_name() {
    local rnd
    rnd="$(od -t u -N 4 /dev/urandom | head -n1 | awk '{print $2 % 1000}')"
    if [ $SHORTNAMES -eq  1 ]; then
        echo "remsh${rnd}"
    else
        echo "remsh${rnd}-${EMQX_NODE_NAME}"
    fi
}

remsh() {
    local tmpnode
    tmpnode="$(gen_tmp_node_name)"
    # shellcheck disable=SC2086
    erl $ERL_NAME_ARG "$tmpnode" \
        -hidden \
        -setcookie "$COOKIE" \
        -remsh "$EMQX_NODE_NAME" \
        $EPMD_ARGS
}

# evaluate erlang expression in remsh node
eval_remsh_erl() {
    local tmpnode erl_code
    tmpnode="$(gen_tmp_node_name)"
    erl_code="$1"
    # shellcheck disable=SC2086 # need to expand EMQD_ARGS
    erl $ERL_NAME_ARG "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$erl_code" 2>&1
}

ctl() {
    if [ -z "${PASSTHROUGH_ARGS:-}" ]; then
        logerr "Need at least one argument for ctl command"
        logerr "e.g. $0 ctl -- help"
        exit 1
    fi
    local args rpc_code output result
    args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")"
    rpc_code="
        case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of
          ok ->
            init:stop(0);
          Error ->
            io:format(\"~p~n\", [Error]),
            init:stop(1)
        end"
    set +e
    output="$(eval_remsh_erl "$rpc_code")"
    result=$?
    if [ $result -eq 0 ]; then
        echo -e "$output"
    else
        logerr "$output"
    fi
    exit $result
}

case "$COMMAND" in
    run)
        boot
        ;;
    remsh)
        remsh
        ;;
    ctl)
        ctl
        ;;
    eval)
        PASSTHROUGH_ARGS=('eval_erl' "${PASSTHROUGH_ARGS[@]}")
        ctl
        ;;
esac
